package com.magenta.maxoptra.gis.service;

import com.magenta.maxoptra.cache.PostalCodesCache;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.Logger;
import com.magenta.maxoptra.gis.datasources.GisDataSource;
import com.magenta.maxoptra.gis.datasources.GisDataSources;
import com.magenta.maxoptra.gis.distancematrix.DistanceMatrixCoordinate;
import com.magenta.maxoptra.gis.format.PostalCodeFormatter;
import com.magenta.maxoptra.gis.format.postal.ParsePostalCodeException;
import com.magenta.maxoptra.gis.utils.LineUnits;
import com.magenta.maxoptra.gis.utils.TimeUnits;
import com.magenta.maxoptra.gis.utils.UnitsConverter;
import com.magenta.mrs.util.DefaultClient;

import javax.ejb.Local;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.naming.NamingException;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONObject;

import java.net.URI;
import java.sql.SQLException;
import java.text.FieldPosition;
import java.text.Format;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Sergey Grachev
 */
@Local(AuxiliaryCacheService.class)
@TransactionManagement(TransactionManagementType.BEAN)
public class AuxiliaryCacheServiceImpl implements AuxiliaryCacheService, AuxiliaryCacheServiceMBean {

    private final Logger logger = new Logger(getClass());
    private final Map<GeoCountry, PostalCodesCache> tables = new HashMap<GeoCountry, PostalCodesCache>();

    private PostalCodesCache getPostalCodesCacheByCountry(final GeoCountry country) {
        synchronized (tables) {
            PostalCodesCache table = tables.get(country);
            if (table == null) {
                tables.put(country, table = new PostalCodesCache(country, logger));
            }
            return table;
        }
    }

    @Override
    public GeoRoute approximatePostalCodesRoute(final PostalCode from, final PostalCode to, final boolean findCoordinates) {
        final PostalCodesCache table = getPostalCodesCacheByCountry(from.getCountry());
        return table != null ? table.getApproximatePostalCodesRoute(from, to, findCoordinates) : null;
    }

    @Override
    public GeoRoute approximateCoordinatesRoute(final GeoCountry country, final GeographicCoordinates from, final GeographicCoordinates to) {
        final PostalCodesCache table = getPostalCodesCacheByCountry(country);
        return table != null ? table.getApproximateCoordinatesRoute(from, to) : null;
    }

    @Override
    public PostalCodeDistanceMatrix postalCodesDistanceMatrix(final List<PostalCode> postalCodes) {
        final PostalCodeDistanceMatrix result = new PostalCodeDistanceMatrix();
        for (final PostalCode from: postalCodes) {
            final PostalCodesCache table = getPostalCodesCacheByCountry(from.getCountry());
            final List<PostalCode> to = new ArrayList<PostalCode>(postalCodes);
            to.remove(from);
            final Map<Object, GeoRoute> row = table.getPostalCodesMatrixRow(from, to);
            if (row != null) {
                result.add(from.getDistanceMatrixKey().getValue(), row);
            }
        }
        return result;
    }

    @Override
    public GeoRoute approximatePostalCodesRoute(final String country, final String from, final String to)
            throws ParsePostalCodeException, SQLException, NamingException, InterruptedException {

        final GeoCountry c = GeoCountry.valueOf(country);
        return approximatePostalCodesRoute(PostalCodeFormatter.parse(c, from), PostalCodeFormatter.parse(c, to), true);
    }

    @Override
    public GeoRoute approximateCoordinatesRoute(final String country, final String fromLat, final String fromLon, final String toLat, final String toLon)
            throws ParsePostalCodeException, SQLException, NamingException, InterruptedException {

        final GeoCountry c = GeoCountry.valueOf(country);
        return approximateCoordinatesRoute(c,
                new DistanceMatrixCoordinate(Double.parseDouble(fromLat), Double.parseDouble(fromLon)),
                new DistanceMatrixCoordinate(Double.parseDouble(toLat), Double.parseDouble(toLon)));
    }

	@Override
	public GeoRoute[][] timeDistanceMatrix(
			List<GeographicCoordinates> coordinates) {
        final GisDataSource ds = GisDataSources.get("time-distance-matrix");
        String request = makeHttpRequest(ds.url(), coordinates);
        long timer = System.nanoTime();
		String requestResult = doRequest(request);
		timer = System.nanoTime() - timer;
    	int size = coordinates.size();
		//System.out.printf("Matrix [%2$s x %2$s] request duration [%1$s] ns.", timer, size).println();
        //Parse request result (JSON).
        try {
        	int negativeElementsCount = 0;
			GeoRoute[][] result = new GeoRoute[size][size];
			int i = 0;
			int j = 0;
        	JSONObject o = new JSONObject(requestResult);
            JSONArray matrix = (JSONArray) o.get("matrix");
			for ( int index = 0, length = matrix.length(); index < length; index++ ) {
            	JSONArray distanceTime = matrix.getJSONArray(index); 
            	
                int meters = distanceTime.getInt(0);
                int seconds = distanceTime.getInt(1);
                Double dist = UnitsConverter.convert(LineUnits.METER, LineUnits.KILOMETRE, (double) meters);
                Long dur = UnitsConverter.convert(TimeUnits.SECOND, TimeUnits.MILLI, (long) seconds);
                
                if ( meters < 0 ) {
                	negativeElementsCount++;
                }
                
                result[i][j] = new GeoRoute(dist, dur, coordinates.get(i), coordinates.get(j));
                j++;
                if ( j >= size ) {
                	i++;
                	j = 0;
                }
            }
			System.out.printf("Matrix [%2$s x %2$s] request duration [%1$s] ns, negative elements [%3$s].", timer, size, negativeElementsCount).println();
            return result;
            
        } catch ( Throwable th ) {
        	logger.error(th);
        }
		
		return null;
	}

	private static final Pattern repeatPart = Pattern.compile(".*(\\(.+\\)\\*)");
    private static final Pattern DECIMAL_INTEGER = Pattern.compile("-?\\d+");

    public static Integer safetyParseAsInteger(String text) {
        if (text != null && (DECIMAL_INTEGER.matcher(text.trim())).matches()) {
            return Integer.valueOf(text.trim());
        }
        return null;
    }
	
	String makeHttpRequest(String template,
			List<GeographicCoordinates> coordinates) {
		List<String> coords = new ArrayList<String>(coordinates.size() * 2);
		for ( GeographicCoordinates point : coordinates ) {
			coords.add(Double.toString(point.getLatitude()));
			coords.add(Double.toString(point.getLongitude()));
		}
		StringBuilder messageTemplate = new StringBuilder();
		int maxArgument = getMaxArgument(template);
		Matcher matcher = repeatPart.matcher(template);
		if ( matcher.find() ) {
			Map<Integer, String> matches = new HashMap<Integer, String>();
			Map<Integer, Integer> indexes = new HashMap<Integer, Integer>();
			do {
				String pattern = matcher.group(1);
				String purePattern = pattern.substring(1, pattern.length() - 2);
				//Variable part of pattern
				int n = maxArgument + 1;
				List<Integer> deltas = detectDeltas(purePattern);
				int maxDelta = Collections.max(deltas);
				StringBuilder expandedTemplate = new StringBuilder();
				
				for ( int k = n, length = coords.size(); k < length; ) {
					String nextLocation = purePattern.replaceAll("\\{n\\}", "{" + k + "}");
					for (  int delta : deltas ) {
						int nextN = k + delta;
						nextLocation = nextLocation.replaceAll("\\{n\\S*\\+\\S*" + delta +"\\S*\\}", "{" + nextN + "}");
					}
					expandedTemplate.append(nextLocation);
					
					k += maxDelta + 1;
				}
				matches.put(matcher.start(1), expandedTemplate.toString());
				indexes.put(matcher.start(1), matcher.end(1));
				matcher.region(0, matcher.start(1));
				
			} while ( matcher.find() );
			
			int index = 0;
			Integer[] keys = matches.keySet().toArray(new Integer[matches.size()]);
			Arrays.sort(keys);
			for ( Integer nextIndex : keys ) {
				messageTemplate.append(template.substring(index, nextIndex));
				String expandedTemplate = matches.get(nextIndex);
				messageTemplate.append(expandedTemplate);
				index = indexes.get(nextIndex);
			}
		}
		else {
			messageTemplate.append(template);
		}
		MessageFormat messageFormat = new MessageFormat(messageTemplate.toString());
		return messageFormat.format(coords.toArray(), new StringBuffer(), new FieldPosition(0)).toString();
	}

	private List<Integer> detectDeltas(String pattern) {
		List<Integer> result = new ArrayList<Integer>();
		for ( int i = 0; i < 11; i++ ) {
			if ( Pattern.compile("\\{n\\S*\\+\\S*" + i +"\\S*\\}").matcher(pattern).find() ) {
				result.add(i);
			}
		}
		return result;
	}

	private static final Pattern MESSAGE_ARGUMENTS_PATTERN = Pattern.compile("\\{\\d+\\}");
	int getMaxArgument(String template) {
		Matcher matcher = MESSAGE_ARGUMENTS_PATTERN.matcher(template);
		int maxArgumentNumber = -1;
		while ( matcher.find() ) {
			int argumentNumber = extractNumber(matcher.group(0));//matcher.group(1)
			if ( argumentNumber > maxArgumentNumber ) {
				maxArgumentNumber = argumentNumber;
			}
		}
		return maxArgumentNumber;
	}

	private static final Pattern ARGUMENT_NUMBER_PATTERN = Pattern.compile("\\{\\S*(\\d+).*");
	private int extractNumber(String argument) {
		Matcher matcher = ARGUMENT_NUMBER_PATTERN.matcher(argument);
		if ( matcher.matches() ) {
			return Integer.valueOf(matcher.group(1));
		}
		return -1;
	}

	private String doRequest(String request) {
        String requestResult = null;
        HttpResponse response = null;
        try {
        	HttpClient client = BasicConnectionPool.createHttpClient();
            logger.trace("Request to matrix service: %s", request);
            URI uri = URI.create(request);
            String encodedUrl = uri.toString();
            HttpGet get = new HttpGet(encodedUrl);
            response = client.execute(get);
            requestResult = EntityUtils.toString(response.getEntity());
        } catch (Exception e) {
            logger.warn("Matrix service answer error = " + e.getMessage() + "}");
        }

        return requestResult;
	}
}
